package co.recloud.ariadne.store;

import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import co.recloud.ariadne.cache.DataCache;
import co.recloud.ariadne.cache.GridCache;
import co.recloud.ariadne.config.Configuration;
import co.recloud.ariadne.model.Block;
import co.recloud.ariadne.model.Location;
import co.recloud.ariadne.server.DataServer;
import co.recloud.ariadne.thread.Main;
import com.sleepycat.je.*;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

public class DataStore {

    private static ConcurrentMap<Location, DataStore> storesSingleton = new ConcurrentHashMap<Location, DataStore>();

    static {
        storesSingleton = new ConcurrentHashMap<Location, DataStore>();
    }

    public static synchronized DataStore getStore(Location location) {
        HostTable ht = HostTable.getInstance();
        if (storesSingleton == null) {
            storesSingleton = new ConcurrentHashMap<Location, DataStore>();
        }
        if (location != null && ht.isReplica(location) && !storesSingleton.containsKey(location)) {
            storesSingleton.putIfAbsent(location, new DataStore(location));
        }
        return storesSingleton.get(location);
    }

    //private GridCache<Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>>> dataCache;
    private Environment dbEnvironment;
    private EntityStore dbStore;
    private PrimaryIndex<Integer, Block> dbIndex;
    private TransactionConfig dbTxnConfig;

    public DataStore(Location location) {
        //dataCache = new DataCache<Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>>>(Configuration.DATA_CACHE_SIZE, Configuration.DATA_FILE_WIDTH, Configuration.DATA_FILE_HEIGHT);
        //dataCache.setStore(new FileStore<Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>>>(
        //        new File(Configuration.DATA_FILE_DIR + "/data_" + location.getX() + "_" + location.getY()), Configuration.DATA_BLOCK_SIZE, Configuration.DATA_FILE_WIDTH, Configuration.DATA_FILE_HEIGHT));
        EnvironmentConfig myEnvConfig = new EnvironmentConfig();
        StoreConfig storeConfig = new StoreConfig();

        myEnvConfig.setAllowCreate(true);
        myEnvConfig.setTransactional(true);
        storeConfig.setAllowCreate(true);
        storeConfig.setTransactional(true);

        try {
            // Open the environment and entity store
            dbEnvironment = new Environment(new File(Configuration.DATA_FILE_DIR + "/data_" + location.getX() + "_" + location.getY()), myEnvConfig);
            dbStore = new EntityStore(dbEnvironment, "EntityStore", storeConfig);
            dbIndex = dbStore.getPrimaryIndex(Integer.class, Block.class);
            dbTxnConfig = new TransactionConfig();
            dbTxnConfig.setSerializableIsolation(false);
            dbTxnConfig.setReadCommitted(true);
        } catch (DatabaseException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private String toPath(String schema, String columnFamily, String key) {
        return schema + "." + columnFamily + ":" + key;
    }

    private boolean checkPath(Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> data, String path, Long time, boolean create) {
        if (checkPath(data, path, create)) {
            boolean hasTime = false;
            if (create) {
                if(!data.get(path).containsKey(time)) {
                    data.get(path)
                            .put(
                                    time,
                                    new TreeMap<Long, Map<String, Object>>());
                }
                hasTime = true;
            } else {
                hasTime = !data.get(path).headMap(time + 1).isEmpty();
            }
            return hasTime;
        }
        return false;
    }

    private void createPath(Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> data, String path, Long time, Long id) {
        if (checkPath(data, path, time, true)) {
            if(!data.get(path).get(time).containsKey(id)) {
            data.get(path).get(time)
                    .put(id, new HashMap<String, Object>());
            }
        }
    }

    public Map<String, Object> get(String schema, String columnFamily,
                                   String key, Long time) {
        String path = toPath(schema, columnFamily, key);
        Integer coords = resolveCoords(path);
        Map<String, Object> result = null;
        try {
            if(dbIndex.contains(coords)) {
                Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> block = dbIndex.get(coords).block;
                if(block != null) {
                    if (checkPath(block, path, time, false)) {
                        SortedMap<Long, SortedMap<Long, Map<String, Object>>> timeMap = block
                                .get(path).headMap(time + 1);
                        SortedMap<Long, Map<String, Object>> idMap = timeMap.get(timeMap.lastKey());
                        result = idMap.get(idMap.lastKey());
                    }
                }
            }
        } catch (DatabaseException e) {
            System.out.println("DataStore, get: " + e.getMessage());
            e.printStackTrace();
        } finally {
        }
        return result;
    }

    private Integer coordsToInteger(int [] coords) {
        return (coords[0] % Configuration.DATA_FILE_WIDTH) * Configuration.DATA_FILE_HEIGHT +
                coords[1] % Configuration.DATA_FILE_HEIGHT;
    }
    private Integer resolveCoords(String path) {
        int[] coords = AddressTable.hash(path);
        return coordsToInteger(coords);
    }

    public Long getTime(String schema, String columnFamily, String key,
                        Long time) {
        Long result = null;
        String path = toPath(schema, columnFamily, key);
        Integer coords = resolveCoords(path);
        try {
            if(dbIndex.contains(coords)) {
                Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> block = dbIndex.get(coords).block;
                if(block != null) {
                    if (checkPath(block, path, time, false)) {
                        SortedMap<Long, SortedMap<Long, Map<String, Object>>> timeMap = block
                                .get(path).headMap(time + 1);
                        result = timeMap.lastKey();
                    }
                }
            }
        } catch (DatabaseException e) {
            System.out.println("DataStore, get: " + e.getMessage());
            e.printStackTrace();
        } finally {
        }
        return result;
    }

    public void put(String schema, String columnFamily, String key, Long time,
                    Long id, Map<String, Object> row) {
        String path = toPath(schema, columnFamily, key);
        Integer coords = resolveCoords(path);
        try {
            Transaction txn = dbEnvironment.beginTransaction(null, dbTxnConfig);
            Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> block = null;
            Block rawBlock = null;
            if(dbIndex.contains(txn, coords, null)) {
                rawBlock = dbIndex.get(txn, coords, null);
                block = rawBlock.block;
            }
            if(block == null) {
                block = new HashMap<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>>();
                rawBlock = new Block();
                rawBlock.coord = coords;
                rawBlock.block = block;
                dbIndex.put(txn, rawBlock);
            }
            createPath(block, path, time, id);
            block.get(path).get(time).get(id).putAll(row);
            SortedMap<Long, SortedMap<Long, Map<String, Object>>> laterVersions = block.get(path).tailMap(time+1);
            for(Long laterTime : laterVersions.keySet()) {
                Map<String, Object> oldRowCopy = new HashMap<String, Object>();
                oldRowCopy.putAll(row);
                Map<String, Object> laterRow = laterVersions.get(laterTime).get(laterVersions.get(laterTime).lastKey());
                oldRowCopy.putAll(laterRow);
                laterRow.putAll(oldRowCopy);
            }
            dbIndex.put(txn, rawBlock);
            txn.commit();
        } catch (DatabaseException e) {
            System.out.println("DataStore, get: " + e.getMessage());
            e.printStackTrace();
        } finally {
        }

    }

    private boolean checkPath(Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> data, String path, boolean create) {
        if (create) {
            if(!data.containsKey(path)) {
                data.put(
                        path,
                        new TreeMap<Long, SortedMap<Long, Map<String, Object>>>());
            }
        } else if (!data.containsKey(path)) {
            return false;
        }
        return true;
    }

    public Set<String> getAllPaths(int x, int y) {
        Set<String> paths = new HashSet<String>();
        int [] rawCoords = {x,y};
        Integer coords = coordsToInteger(rawCoords);
        try {
            if(dbIndex.contains(coords)) {
                Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> block = dbIndex.get(coords).block;
                if(block != null) {
                    paths.addAll(block.keySet());
                }
            }
        } catch (DatabaseException e) {
            System.out.println("DataStore, get: " + e.getMessage());
            e.printStackTrace();
        } finally {
        }
        return paths;
    }

    public SortedMap<Long, SortedMap<Long, Map<String, Object>>> getAllVersions(
            String path) {
        Integer coords = resolveCoords(path);
        SortedMap<Long, SortedMap<Long, Map<String, Object>>> versions = new TreeMap<Long, SortedMap<Long, Map<String, Object>>>();
        try {
            if(dbIndex.contains(coords)) {
                Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> block = dbIndex.get(coords).block;
                for (Long time : block.get(path).keySet()) {
                    versions.put(time, new TreeMap<Long, Map<String, Object>>());
                    for (Long id : block.get(path).get(time).keySet()) {
                        versions.get(time).put(id, new HashMap<String, Object>());
                        versions.get(time).get(id)
                                .putAll(block.get(path).get(time).get(id));
                    }
                }
            }
        } catch (DatabaseException e) {
            System.out.println("DataStore, get: " + e.getMessage());
            e.printStackTrace();
        } finally {
        }
        return versions;
    }

    public void condensePath(String path) {
        SortedSet<Long> activeTimes = DataServer.getActiveStartTimes();
        activeTimes.add(Main.getTime());
        SortedMap<Long, SortedMap<Long, Map<String, Object>>> versions = getAllVersions(path);
        Set<Long> timesToKeep = new HashSet<Long>();
        Set<Long> timesToRemove = new HashSet<Long>();
        timesToRemove.addAll(versions.keySet());
        for (Long time : activeTimes) {
            SortedMap<Long, SortedMap<Long, Map<String, Object>>> headMap = versions.headMap(time + 1);
            if (!headMap.isEmpty()) {
                Long timeToKeep = headMap.lastKey();
                timesToKeep.add(timeToKeep);
                Set<Long> idsToRemove = new HashSet<Long>();
                idsToRemove.addAll(versions.get(timeToKeep).keySet());
                idsToRemove.remove(versions.get(timeToKeep).lastKey());
                for (Long id : idsToRemove) {
                    versions.get(timeToKeep).remove(id);
                }
            }
        }
        timesToRemove.removeAll(timesToKeep);
        for (Long time : timesToRemove) {
            versions.remove(time);
        }
    }

    public static Collection<DataStore> getAllStores() {
        return storesSingleton.values();
    }
    
    public Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> getBlock(int block) {
        AddressTable at = AddressTable.getInstance();
        int[] coords = AddressTable.hash(block);
        Integer resolved = coordsToInteger(coords);
        Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> data = null;
        try {
            if(dbIndex.contains(resolved)) {
                data = dbIndex.get(resolved).block;
            }
        } catch (DatabaseException e) {
            System.out.println("DataStore, getBlock: " + e.getMessage());
        }
        return data;
    }

    public void putBlock(int address, Map<String, SortedMap<Long, SortedMap<Long, Map<String, Object>>>> data) {
        AddressTable at = AddressTable.getInstance();
        int[] coords = AddressTable.hash(address);
        Integer resolved = coordsToInteger(coords);
        try {
            Block block = new Block();
            block.coord = resolved;
            block.block = data;
            dbIndex.put(block);
        } catch (DatabaseException e) {
            System.out.println("DataStore, putBlock: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    public int[] getOpenBlock() {
        int[] coords = null;
        for(int x = 0; x < Configuration.DATA_FILE_WIDTH; x++) {
            for(int y = 0; y < Configuration.DATA_FILE_HEIGHT; y++) {
                coords[0] = x;
                coords[1] = y;
                Integer resolved = coordsToInteger(coords);
                try {
                    if(dbIndex.contains(resolved)) {
                        return coords;
                    }
                } catch (DatabaseException e) {
                    System.out.println("DataStore, getOpenBlock: " + e.getMessage());
                }
            }
        }
        return coords;
    }

    public void freeBlock(int address) {
        AddressTable at = AddressTable.getInstance();
        int[] coords = AddressTable.hash(address);
        Integer resolved = coordsToInteger(coords);
        try {
            dbIndex.delete(resolved);
        } catch (DatabaseException e) {
            System.out.println("DataStore, freeBlock: " + e.getMessage());
        }
    }
}
